/**
 * 
 * 
 */
(function() {
    'use strict';

    /**
     * @name castExtensionID 
     * @var 
     */
    var castExtensionID = "boadgeojelhgndaghljhdicfkmllpafd";
    /**
     * Constants of states for Chromecast device
     * 
     * @name DEVICE_STATE 
     * @namespace   
     */
    var DEVICE_STATE = {
	/**
	 * Default state indicating that Cast extension is installed,
	 * but showing no current activity
	 * @member
	 */
	'IDLE'    : 0,
	/**
	 * Shown when Chrome has one or more local activities running
	 * on a receiver
	 * @member
	 */
	'ACTIVE'  : 1,
	/**
	 * Shown when the device is actively being used, but when
	 * one or more issues have occurred
	 * @member
	 */
	'WARNING' : 2,
	/**
	 * Should not normally occur, but shown when there is a failure
	 * @member
	 */
	'ERROR'   : 3,
	/**
	 * When we don't know
	 * @member
	 */
	'UNKNOWN' : 4
    };

    /**
     * Constants of states for CastPlayer
     *
     * @name PLAYER_STATE 
     * @namespace
     **/
    var PLAYER_STATE = {
	/** 
	 * @member
	 */
	'IDLE'    : 'IDLE',
	/** 
	 * @member
	 */
	'LOADING' : 'LOADING',
	/** 
	 * @member
	 */
	'LOADED'  : 'LOADED',
	/** 
	 * @member
	 */
	'PLAYING' : 'PLAYING',
	/** 
	 * @member
	 */
	'PAUSED'  : 'PAUSED',
	/** 
	 * @member
	 */
	'STOPPED' : 'STOPPED',
	/** 
	 * @member
	 */
	'SEEKING' : 'SEEKING',
	/** 
	 * @member
	 */
	'ERROR'   : 'ERROR'
    };
    //================================================================
    /** 
     * @name __Log 
     * @namespace 
     *
     * Beautify logs 
     */
    var Log = { 
	/** Errors */
	e: console.error.bind(console, "%cCastControl", "color:#c00"), 
	/** Warnings */
	w: console.warn.bind(console, "%cCastControl", "color:#cc0"), 
	/** Information */
	i: console.log.bind(console, "%cCastControl", "color:#00c"),
	/** Debug */
	d: console.log.bind(console, "%cCastControl", "color:#c0c")
	// i: function () {}
    };

    //================================================================
    /**
     * Cast player object.
     *
     * main variables:
     *  - deviceState for Cast mode:
     *  - Cast player variables for controlling Cast mode media playback
     *  - Current media variables for transition between Cast and local modes
     *
     * @public 
     * @name CastControl
     * @class 
     * @constructor 
     */
    var CastControl = function() {
	/*
	 * @type string 
	 *
	 * default set to the default media receiver app ID optional:
	 * you may change it to point to your own
	 * @fieldOf CastControl.prototype
	 */
	this.mApplicationID = null;


	/** 
	 * Structure holding the media information 
	 * @name mMedia
	 * @fieldOf CastControl.prototype
	 */
	this.mMedia = { 
	    /**
             * a chrome.cast.media.Media object
             * @fieldOf CastControl.prototype.mMedia
             */
	    mSession : null,
	    /**
             * A number for current media index
             * @fieldOf CastControl.prototype.mMedia
             */
	    mInfo : null,
	    /**
             * A number for current media time
             * @fieldOf CastControl.prototype.mMedia
             */
	    mTime : 0,
	    /**
             * A number for current media duration
             * @fieldOf CastControl.prototype.mMedia
             */
	    mDuration : -1,
	};

	/**
	 * Player stuff
	 * @name mPlayer
	 * @fieldOf CastControl.prototype
	 */
	this.mPlayer = {
	    /**
             * A state for Cast media player
             * @fieldOf CastControl.prototype.mPlayer
             */
	    mState : PLAYER_STATE.IDLE,
	    /**
             * A flag for autoplay after load
             * @fieldOf CastControl.prototype.mPlayer
             */
	    mAutoplay : false,
	    /**
             * Audio on and off
             * @fieldOf CastControl.prototype.mPlayer
             */
	    mLive : false,
	    /**
             * A timer for tracking progress of media
             * @fieldOf CastControl.prototype.mPlayer
             */
	    mTimer : null,
	    /**
             * A boolean to stop timer update of progress
             * @fieldOf CastControl.prototype.mPlayer
             */
	    // when triggered by media status event
	    mProgressFlag : true,
	    /**
             * A number in milliseconds for minimal
             * @fieldOf CastControl.prototype.mPlayer
             */
	    // progress update
	    mTimerStep : 1000,
	};
	/** 
	 * Structure holding device information 
	 * @name mDevice
	 * @fieldOf CastControl.prototype
	 */
	this.mDevice = { 
	    /* device variables */
	    /**
             * A state for device
             * @fieldOf CastControl.prototype.mDevice
             */
	    mState : DEVICE_STATE.UNKNOWN,
	    /**
             * volume
             * @fieldOf CastControl.prototype.mDevice
             */
	    mVolume : 0.5,
	    /* Current media variables */
	    /**
             * Audio on and off
             * @fieldOf CastControl.prototype.mDevice
             */
	    mMuted : false
	}
	/**
	 * a chrome.cast.Session object
	 * @fieldOf CastControl.prototype
	 */
	this.mSession = null;

	/**
	 * Structure holding UI elements 
	 * @name mUI
	 * @fieldOf CastControl.prototype
	 */
	this.mUI = { 
	    /* UI elements */
	    /**
             * Play UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mPlay : null,
	    /**
             * Pause UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mPause : null,
	    /**
             * Current UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mCurrent : null,
	    /**
             * Seek UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mSeekBar : null,
	    /**
             * Seek UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mSeek : null,
	    /**
             * Seek UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mSeekBg : null,
	    /**
             * Duration UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mDuration : null,
	    /**
             * AudioOn UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mAudioOn : null,
	    /**
             * AudioOff UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mAudioOff : null,
	    /**
             * Audio UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mAudio : null,
	    /**
             * Audio UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mAudioBg : null,
	    /**
             * Rate UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mRate : null,
	    /**
             * Idle UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mIdle : null,
	    /**
             * Error UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mError : null,
	    /**
             * Warning UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mWarning : null,
	    /**
             * Active UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mActive : null,
	    /**
             * Unknown UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mUnknown : null,
	    /**
             * State UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mState : null,
	    /**
             * File UI element
             * @fieldOf CastControl.prototype.mUI
             */
	    mFile : null,
	};

	// Initialize the stuff
	this.initialize();
    };
    // === Receiver ==================================================
    /**
     * Function to check for ChromeCast extension and/or Chrome
     * before doing anything else.
     */
    CastControl.prototype.initialize = function() {

	var hash    = location.hash || null;
	var search  = location.search;
	if (search && search.charAt(0) == '?') 
	    search = search.substring(1,search.length);
	if (hash && hash.charAt(0) == '#') 
	    hash = hash.substring(1,hash.length);

	var opts    = search.split('&');
	var options = { // Set defaults 
	    auto:   this.mPlayer.mAutoplay,
	    live:   this.mPlayer.mLive,
	    volume: this.mDevice.mVolume*100,
	    muted:  this.mDevice.mMuted,
	    debug:  false
	};
	for (var i = 0; i < opts.length; i++) {
	    var pair = opts[i].split('=');
	    var key  = pair[0];
	    var val  = pair[1];
	    if (!key || key.length <= 0) continue;
	    if (!val || val.length <= 0) val = true;
	    options[key] = val;
	    Log.d("Got option: " + key + "=" + val);
	}
	this.mPlayer.mAutoplay = options["auto"];
	this.mPlayer.mLive     = options["live"];
	this.mDevice.mVolume   = parseInt(options["volume"])/100;
	this.mDevice.mMuted    = options["mute"];
	if (hash != null && hash.length > 0) {
	    Log.d("Got hash=" + hash);
	    this.mApplicationID = hash;
	}
	if (!options["debug"]) 
	    /** @ignore */
	    Log.d = function() {};
	Log.i("Initialisning");
	this.initializeCastControl();
    };
    /**
     * Initialize Cast media player.
     *
     * Initializes the API. Note that either successCallback and
     * errorCallback will be invoked once the API has finished
     * initialization. The sessionListener and receiverListener may be
     * invoked at any time afterwards, and possibly more than once.
     */
    CastControl.prototype.initializeCastControl = function() {
	if (!chrome.cast || !chrome.cast.isAvailable) {
	    this.initializeUI();
	    setTimeout(this.initializeCastControl.bind(this), 1000);
	    return;
	}

	// request session
	if (this.mApplicationID == null)
	    this.mApplicationID = 
	    chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID;
	Log.i("Application ID=", this.mApplicationID + " default: " 
		      + chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
	var sessionRequest = 
	    new chrome.cast.SessionRequest(this.mApplicationID);
	var apiConfig =
	    new chrome.cast.ApiConfig(sessionRequest,
				      this.sessionListener.bind(this),
				      this.receiverListener.bind(this),
				      chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED);

	chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this),
			       this.onInitError.bind(this));

	// this.addVideoThumbs();
    };
    /**
     * Callback function for init success
     */
    CastControl.prototype.onInitSuccess = function() {
	Log.d("init success");
	this.mDevice.mState = DEVICE_STATE.IDLE;
	this.updateMediaControlUI();
    };
    /**
     * Callback function on init failure
     *
     * @param {chrome.cast.Error} e Error description
     */
    CastControl.prototype.onInitError = function(e) {
	Log.w("Failed to initialize API: " + e.description);
	this.user("ChromeCast extension failed");
	this.mDevice.mState = DEVICE_STATE.ERROR;
	this.updateMediaControlUI();
	this.mUI.mError.title = e.description;
    };
    /**
     * Generic error callback function
     *
     * @param {String}            w A 'what' prefix 
     * @param {cast.chrome.Error} e Error 
     */
    CastControl.prototype.onError = function(w,e) {
	Log.e("[" + (w ? w  : "") + "] " +
		       (e ? e.description : "?"));
    };
    /**
     * This handles auto-join when a page is reloaded. When active
     * session is detected, playback will automatically join existing
     * session and occur in Cast mode and media status gets synced up
     * with current media of the session
     *
     * @param {chrome.cast.Session} e Session
     */
    CastControl.prototype.sessionListener = function(e) {
	this.mSession = e;
	Log.d("Session listener found a session " + e);
	this.sessionUpdateListener(this.mSession); // Force update
	if (this.mSession) {
	    if (this.mSession.media[0]) {
		var meta = e.media[0].media.metadata;
		this.mMedia.mInfo = {"title": (meta ? meta['title'] : '?'),
				     "thumb": (meta && meta['images'] ?
					       meta['images'][0]['url']
					       : '?'),
				     "url": e.media[0].media.contentId };
		this.mediaListener('activeSession', this.mSession.media[0]);
	    }
	    this.mSession.addMediaListener(this.mediaListener.
					  bind(this,'activeSession'));
	    this.mSession.addUpdateListener(this.sessionUpdateListener
					   .bind(this));
	    var ns = "";
	    Log.d('Number of namespace: ' + this.mSession.namespaces.length);
	    for (var i = 0; i < ns.length; i++) {
		Log.d("Namespace: " +
			      this.mSession.namespaces[i]['name']);
		this.mSession.addMessageListener(ns,
						this.messageListener
						.bind(this));
	    }
	    if (this.mSession.appImages) {
		Log.d('Number of images: ' + this.mSession.appImages.length);
		for (var i = 0; i < this.mSession.appImages.length; i++) {
		    Log.d("Image: " + this.mSession.appImages[i].url);
		}
	    }
	    Log.i("Name='" + this.mSession.displayName + "' " +
			  "Status='" + this.mSession.statusText + "'");
	}
	else {
	    this.mDevice.mState = DEVICE_STATE.IDLE;
	}
    };
    /**
     * Call-back for session updates
     *
     * @param {Boolean} isAlive True if still alive
     */
    CastControl.prototype.sessionUpdateListener = function(isAlive) {
	Log.i(isAlive ? "Session updated" : "Session removed");

	if (!isAlive) {
	    this.mSession = null;
	    this.mMedia.mSession  = null;
	    this.mMedia.mTime     = 0;
	    this.mMedia.mDuration = 0;
	    this.mDevice.mState = DEVICE_STATE.IDLE;
	    this.updateDisplayMessage();
	    this.updateMediaControlUI();
	    this.updateProgressUI();
	    return;
	}

	this.mDevice.mState   = DEVICE_STATE.ACTIVE;
	this.mDevice.mVolume  = this.mSession.receiver.volume.level;
	this.mDevice.mMuted   = this.mSession.receiver.volume.muted;
	Log.d('Volume: ' + this.mDevice.mVolume +
		      " Muted: " + this.mDevice.mMuted);
	this.updateAudioUI();
    };
    /** 
     * Listen for incoming messages
     * 
     * @param namespace  The message namespace 
     * @param message    The message 
     */
    CastControl.prototype.messageListener = function(namespace,	message) {
	Log.d("Got message: " + namespace + ": '" + message + "'");
    };
    /**
     * This indicates availability of receivers but does not provide a
     * list of device IDs
     *
     * @param {chrome.cast.ReceiverAvailability} e Receiver availability.
     *
     */
    CastControl.prototype.receiverListener = function(e) {
	if (e === chrome.cast.ReceiverAvailability.AVAILABLE) {
	    Log.d("Receiver found");
	}
	else {
	    Log.w("Receiver list empty");
	    this.mDevice.mState = DEVICE_STATE.UNKNOWN;
	}
	this.updateMediaControlUI();
	this.updateDisplayMessage();
    };

    // === App =======================================================
    /**
     * Requests that a receiver application session be created or
     * joined. By default, the SessionRequest passed to the API at
     * initialization time is used; this may be overridden by passing
     * a different session request in opt_sessionRequest.
     */
    CastControl.prototype.launchApp = function() {
	Log.i("Launching app...");
	chrome.cast.requestSession(this.onLaunchSuccess.bind(this),
				   this.onLaunchError.bind(this));
	if (this.mPlayer.mTimer) {
	    clearInterval(this.mPlayer.mTimer);
	}
    };
    /**
     * Callback function for request session success.
     *
     * @param {chrome.cast.Session} e New session
     */
    CastControl.prototype.onLaunchSuccess = function(e) {
	Log.d("Launch success: " + e.sessionId);
	this.mSession = e;
	this.mDevice.mState = DEVICE_STATE.ACTIVE;
	this.updateMediaControlUI();
    };
    /**
     * Callback function for launch error
     *
     * @param {chrome.cast.Error} e Error description
     */
    CastControl.prototype.onLaunchError = function(e) {
	Log.w("Launch error: " + e.description);
	this.user("Application failed to start");
	this.mDevice.mState = DEVICE_STATE.ERROR;
	this.mUI.mError.title = e.description;

    };
    /**
     * Stops the running receiver application associated with the
     * session.
     */
    CastControl.prototype.stopApp = function() {
	this.mSession.stop(this.onStopAppSuccess.bind(this, 'Session stopped'),
			  this.onStopAppSuccess.bind(this));
    };
    /**
     * Callback function for stop app success
     *
     * @param {String} message User message 
     */
    CastControl.prototype.onStopAppSuccess = function(message) {
	Log.d(message);
	this.mDevice.mState     = DEVICE_STATE.IDLE;
	this.mPlayer.mState     = PLAYER_STATE.IDLE;
	this.mMedia.mSession = null;
	clearInterval(this.mPlayer.mTimer);
	this.updateDisplayMessage();
	this.updateMediaControlUI();
    };
    /** 
     * Called when failing to stop the app 
     * 
     * @param error {chrome.cast.Error} Error 
     */
    CastControl.prototype.onStopAppError = function(error) {
	var desc =  "";
	var fwd  = false;
	switch  (error.code) { 
	case chrome.cast.ErrorCode.TIMEOUT: 
	    desc="Timeout" ; 
	    break;
	case chrome.cast.ErrorCode.API_NOT_INITIALIZED: 
	    desc="Not initialized"; 
	    fwd = false;
	    break;
	case chrome.cast.ErrorCode.CHANNEL_ERROR: 
	    desc="Channel error";
	    break;
	case chrome.cast.ErrorCode.SESSION_ERROR: 
	    desc="Session error";
	    break;
	case chrome.cast.ErrorCode.EXTENSION_MISSING:
	    desc="No extension";
	    fwd = false;
	    break;	    
	default:
	    desc="Unknown";
	    break;
	}
	Log.w("Failed to stop app: " + 
	      (error && error.description ? 
	       error.description : desc));
	this.onStopAppSuccess("Failed to stop");
    }

    // === Media =====================================================
    /**
     * Select media
     *
     * @param {Object} json Media information
     *
     * @a info should be like
     * @verbatim
     { "title":  <TITLE>,
     "thumb":  <THUMB_URL>,
     "tiny":   <MEDIA_URL>
     "low":    <MEDIA_URL>
     "medium": <MEDIA_URL>
     "high":   <MEDIA_URL>
     }
     * @endverbatim
     */
    CastControl.prototype.selectMedia = function(json) {
	var url      = null;
	var title    = null;
	var thumb    = null;
	try {
	    // var json     = JSON.parse(streams);
	    var idx  = this.mUI.mRate.selectedIndex;
	    title    = json['title'];
	    thumb    = json['thumb'];
	    // var links    = json['links'];
	    for (var i = idx; i < this.mUI.mRate.length; i++) {
		var val = this.mUI.mRate.options[i].value;
		if (!json[val]) continue;

		url = json[val];
		break;
	    }
	}
	catch (e) {
	    Log.w("Error selecting media: " + e);
	}
	if (!url) {
	    Log.w("No valid media URL found");
	    this.user("Invalid media");
	    return;
	}
	this.setMediaInfo({"url": url, "title": title, "thumb": thumb});
    };
    /**
     * Update the display of select.
     */
    CastControl.prototype.updateRate = function() {
    };
    /**
     * Select a media content.
     *
     * @param {Object} mediaInfo Media information.
     *
     * @a mediaInfo should be like
     *
     * @verbatim
     { "title": <TITLE>,
     "thumb": <THUMB_URL>,
     "url":   <MEDIA_URL>
     }
     * @endverbatim
     */
    CastControl.prototype.setMediaInfo = function(mediaInfo) {
	Log.i("Media selected: " + JSON.stringify(mediaInfo));

	// Set our information
	this.mMedia.mInfo = mediaInfo;

	// reset currentMediaTime
	this.mMedia.mTime = 0;

	// Load the media 
	this.loadMedia();
    };
    /**
     * Loads media into a running receiver application.
     *
     */
    CastControl.prototype.loadMedia = function() {
	if (!this.mSession) {
	    Log.w("No ChromeCast session");
	    this.user("No ChromeCast session");
	    return;
	}
	if (!this.mMedia.mInfo) {
	    Log.w("No Media loaded");
	    this.user("No Media loaded");
	    return;
	}
	Log.i("Loading..." + this.mMedia.mInfo['title']);

	try {
	    /* var payload = {
	       "title:" : this.mMedia.mInfo['title'],
	       "thumb"  : this.mMedia.mInfo['thumb']
	       }; 
	       
	       var json = {
	       "payload" : payload
	       }; */
	    var payload = { 
		"title"            : this.mMedia.mInfo['title'],
		"thumb"            : this.mMedia.mInfo['thumb'],
		"description"      : this.mMedia.mInfo['title'],
		"expectedDuration" : "0.",
		"sessionCookie"    : "",
	    };
	    var json = { 
		"payload" : payload
	    };
	    
	    var thumb     = this.mMedia.mInfo['thumb'];
	    var metaData  =	new chrome.cast.media.GenericMediaMetadata();
	    metaData.title        = this.mMedia.mInfo['title'];
	    metaData.subtitle     = null;
	    metaData.releaseYear  = null;
	    metaData.releaseDate  = null;
	    // metaData.images       = (thumb != null ? 
	    // 				[new chrome.castImage(thumb)] :
	    //                          null);
	    metaData.type         = chrome.cast.media.MetadataType.GENERIC;
	    Log.d("Metadata: " + JSON.stringify(metaData, " "));
	    
	    var url               = this.mMedia.mInfo['url'];
	    var mediaInfo         = new chrome.cast.media.MediaInfo(url);
	    if (url.lastIndexOf(".m3u8") >= 0) {
		mediaInfo.streamType  = chrome.cast.media.StreamType.LIVE;
		mediaInfo.contentType = "application/x-mpegurl";
	    }
	    else {
		mediaInfo.streamType  = chrome.cast.media.StreamType.BUFFERED;
		mediaInfo.contentType = 'video/mp4';
	    }
	    mediaInfo.metadata    = metaData;	
	    Log.d("Media Info: " + JSON.stringify(mediaInfo, " "));
	    
	    var request = new chrome.cast.media.LoadRequest(mediaInfo);
	    request.autoplay    = this.mPlayer.mAutoplay;
	    request.currentTime = 0;
	    request.customData  = json;
	    // Log.d("Loading with auto=" + this.mPlayer.mAutoplay);
	    this.mPlayer.mState = PLAYER_STATE.LOADING;
	    this.mSession.loadMedia(request,
				    this.mediaListener.bind(this, 'loadMedia'),
				    this.onLoadMediaError.bind(this));
	} catch(e) { 
	    Log.e("Failed to load media");
	    this.onLoadMediaError(e);
	}
	this.updateDisplayMessage();
    };
    /**
     * Callback function for loadMedia success.
     *
     * @param {String}                  how   Custom prefix
     * @param {chrome.cast.media.Media} media A new media object.
     */
    CastControl.prototype.mediaListener = function(how, media) {
	Log.d("New media session ID:" +
		      media.mediaSessionId + ' (' + how + ')');
	this.mMedia.mSession    = media;
	this.mMedia.mTime       = media.currentTime;
	this.mPlayer.mState     = media.playerState;
	if (media.media && media.media.metadata) {
	    var  meta                  = media.media.metadata;
	    if (meta['title'] != null)
		this.mMedia.mInfo['title'] = meta['title'];
	    if (meta['images'] != null && meta['images'].length > 0) 
		this.mMedia.mInfo['thumb'] = meta['images'][0]["url"];
	    Log.d("Title of media " + meta['title']);
	    Log.d("First image url: " + this.mMedia.mInfo['thumb']);
	}


	Log.d('Player state is ' + this.mPlayer.mState);
	if (this.mPlayer.mState == PLAYER_STATE.PLAYING) {
	    // Start progress timer
	    this.startProgressTimer(this.incrementMediaTime);
	}

	this.mMedia.mSession
	    .addUpdateListener(this.mediaStatusListener.bind(this));
	this.mMedia.mDuration = this.mMedia.mSession.media.duration;

	this.updateMediaInfoUI();
	this.updateMediaControlUI();
	this.updateDisplayMessage();
    };
    /**
     * Callback function when media load returns error
     *
     * @param {chrome.cast.Error} e Exception thrown
     */
    CastControl.prototype.onLoadMediaError = function(e) {
	Log.w("Media error: " + JSON.stringify(e, " "));
	this.user("Error loading media");
	this.mPlayer.mState = PLAYER_STATE.IDLE;
	// update UIs
	this.updateMediaControlUI();
	this.updateDisplayMessage();
    };
    /**
     * Callback function for media status update from receiver
     *
     * @param {!Boolean} e true/false
     */
    CastControl.prototype.mediaStatusListener = function(e) {
	Log.d("Media update");
	if (e == false) {
	    // Media died
	    this.mMedia.mTime = 0;
	    this.mPlayer.mState = PLAYER_STATE.IDLE;
	}
	else {
	    var oldState          = this.mPlayer.mState;
	    var newState          = this.mMedia.mSession.playerState;
	    this.mMedia.mTime = this.mMedia.mSession.currentTime;
	    this.mPlayer.mState  = newState;
	    // Check for transition to PLAYING
	    if (oldState != PLAYER_STATE.PLAYING &&
		newState == PLAYER_STATE.PLAYING)
		this.startProgressTimer(this.incrementMediaTime);
	}
	// Do now allow updates from timer
	this.mPlayer.mProgressFlag = false;
	// don't update progress in 1 second
	setTimeout(this.setProgressFlag.bind(this),1000);

	this.updateDisplayMessage();
	this.updateMediaControlUI();
    };
    // === Play-back =================================================
    /** 
     * Call-back on succesfull play request
     * 
     * @param {!PLAYER_STATE} tgt The target state 
     */
    CastControl.prototype.onPlaybackSuccess = function(tgt) {
	this.mPlayer.mState = tgt;
	Log.d("Changed state to " + tgt + " for " +
		      this.mMedia.mSession.sessionId);
	switch (this.mPlayer.mState) {
	case PLAYER_STATE.PLAYING:
	    this.mMedia.mSession
		.addUpdateListener(this.mediaStatusListener.bind(this));
	    // start progress timer - should we do this here?
	    this.startProgressTimer(this.incrementMediaTime);
	    break;
	case PLAYER_STATE.PAUSED:
	case PLAYER_STATE.STOPPED:
	    clearInterval(this.mPlayer.mTimer);
	    break;
	}
	this.updateMediaControlUI();
	this.updateDisplayMessage();
    };
    /**
     * Play media in Cast mode
     */
    CastControl.prototype.playMedia = function() {
	if (!this.mSession) {
	    Log.e("No ChromeCast session");
	    this.user("No ChromeCast session");
	    return;
	}
	if (!this.mMedia.mSession) {
	    Log.w("No current media session. Forgot to load?");
	    this.user("No media loaded");
	    return;
	}

	switch (this.mPlayer.mState)
	{
	case PLAYER_STATE.LOADED:
	case PLAYER_STATE.PAUSED:
	    this.mMedia.mSession
		.play(null,
		      this.onPlaybackSuccess.bind(this,PLAYER_STATE.PLAYING),
		      this.onError.bind(this, "Play"));
	    break;
	default:
	    Log.e("Cannot play in state ");
	    break;
	}
    };
    /**
     * Pause media playback in Cast mode
     */
    CastControl.prototype.pauseMedia = function() {
	if (!this.mMedia.mSession) {
	    Log.e("No media session to pause");
	    return;
	}

	if( this.mPlayer.mState == PLAYER_STATE.PLAYING ) {
	    this.mMedia.mSession
		.pause(null,
		       this.onPlaybackSuccess.bind(this,PLAYER_STATE.PAUSED),
		       this.onError.bind(this, "Pause"));
	}
    };
    /**
     * Stop meia playback in either Cast or local mode
     */
    CastControl.prototype.stopMedia = function() {
	if (!this.mMedia.mSession) {
	    Log.e("No media session to stop");
	    return;
	}

	this.mMedia.mSession
	    .stop(null,
		  this.onPlaybackSuccess.bind(this,PLAYER_STATE.STOPPED),
		  this.onError.bind(this, "Stop"));
    };

    // === Audio =====================================================
    /**
     * On succesful setting audio
     *
     * @param {Boolean} muted If we're muted 
     * @param {Float}   lvl   Current volume level
     */
    CastControl.prototype.onAudioSuccess = function(muted,lvl)  {
	this.mDevice.mMuted  = muted;
	this.mDevice.mVolume = lvl;
	this.updateAudioUI();
    };
    /**
     * Set media volume in Cast mode.
     *
     * @param {Boolean} mute A boolean
     */
    CastControl.prototype.setReceiverVolume = function(mute) {

	// Set pos
	var pos = this.mUI.mAudioBg.value = this.mUI.mAudio.value;
	if (!this.mMedia.mSession) {
	    Log.w("No current media session to adjust audio for");
	    return;
	}

	var vol = 1;
	// add a drag to avoid loud volume
	if (pos < 100) {
	    // var vScale = this.mDevice.mVolume * 100;
	    // if (pos > vScale) pos = vScale + 2*(pos - vScale)/3;

	    vol = pos/100;
	}

	if (!mute)
	    this.mSession
	    .setReceiverVolumeLevel(vol,
				    this.onAudioSuccess.bind(this,mute,vol),
				    this.onError.bind(this, "Volume"));
	else
	    this.mSession.setReceiverMuted(true,
					   this.onAudioSuccess.bind(this,
								    mute,vol),
					   this.onError.bind(this, "Mute"));
    };
    /**
     * Mute media function in either Cast or local mode
     */
    CastControl.prototype.muteMedia = function() {
	// this.mDevice.mMuted = !this.mDevice.mMuted;

	if( this.mMedia.mSession ) {
	    this.setReceiverVolume(!this.mDevice.mMuted);
	}
	// this.updateMediaControlUI();
    };
    // === Seeks =====================================================
    /**
     * @param {Boolean} disabled If true, disable seeks 
     */
    CastControl.prototype.disableSeeks = function(disabled)
    {
	if (disabled) {
	    Log.w("Disabling seek bar");
	    this.mUI.mSeekBar.style.display  = "none";
	    this.mUI.mCurrent.style.display  = "none";
	    this.mUI.mDuration.style.display = "none";
	    // document.getElementById("seekRow").style.display = "none";
	    // this.mUI.mCurrent.style.color = this.mUI.mDuration.style.color =
	    //   this.mUI.mDuration.style.backgroundColor;
	    // this.mUI.mCurrent.title = this.mUI.mDuration.title = "";
	}
	else {
	    Log.d("Enabling seek bar");
	    this.mUI.mSeekBar.style.display  = "block";
	    this.mUI.mCurrent.style.display  = "table-cell";
	    this.mUI.mDuration.style.display = "table-cell";
	    // this.mUI.mCurrent.title = "-:--";
	    // this.mUI.mDuration.title = "0:00";
	}
    };
    /**
     * media seek function in either Cast or local mode.
     *
     * @param {Event} e An event object from seek
     */
    CastControl.prototype.seekMedia = function(event) {
	// Set pos here
	var pos  = this.mUI.mSeekBg.value = this.mUI.mSeek.value;
	var curr = parseInt(this.mMedia.mDuration * pos / 100);

	if( this.mPlayer.mState != PLAYER_STATE.PLAYING &&
	    this.mPlayer.mState != PLAYER_STATE.PAUSED ) {
	    Log.w('Cannot seek in state ' + this.mPlayer.mState);
	    // this.mUI.mSeek.value = this.mUI.mSeekBg.value = 0;
	    return;
	}

	this.mMedia.mTime = curr;

	var request = new chrome.cast.media.SeekRequest();
	request.currentTime = this.mMedia.mTime;
	this.mMedia.mSession
	    .seek(request, this.onSeekSuccess.bind(this, this.mPlayer.mState),
		  this.onError.bind(this, "Seek"));
	this.mPlayer.mState = PLAYER_STATE.SEEKING;
	this.updateDisplayMessage();
    };
    /**
     * Callback function for seek success.
     *
     * @param {PLAYER_STATE} old The state we came from before seeking
     */
    CastControl.prototype.onSeekSuccess = function(old) {
	Log.d('Media seek done, state now ' + old);
	this.mPlayer.mState = old;
	this.updateDisplayMessage();
	this.updateMediaControlUI();
    };
    // === Progress updates ==========================================
    /**
     * Helper function.
     *
     * Increment media current position by 1 second
     */
    CastControl.prototype.incrementMediaTime = function() {
	if (this.mPlayer.mState == PLAYER_STATE.PLAYING) {
	    if( this.mMedia.mTime < this.mMedia.mDuration ) {
		this.mMedia.mTime += 1;
		if (this.mPlayer.mProgressFlag) this.updateProgressUI();
	    }
	    else {
		this.mMedia.mTime = 0;
		clearInterval(this.mPlayer.mTimer);
		this.mDevice.mState = DEVICE_STATE.IDLE;
		this.mPlayer.mState = PLAYER_STATE.IDLE;
		this.updateDisplayMessage();
		this.updateMediaControlUI();
	    }
	}
    };
    /**
     * Set progressFlag with a timeout of 1 second to avoid UI update
     * until a media status update from receiver
     */
    CastControl.prototype.setProgressFlag = function() {
	this.mPlayer.mProgressFlag = true;
	this.updateProgressUI();
    };
    /**
     * @param {function} callback A function to start timer
     */
    CastControl.prototype.startProgressTimer = function(callback) {
	if( this.mPlayer.mTimer ) {
	    clearInterval(this.mPlayer.mTimer);
	    this.mPlayer.mTimer = null;
	}
	// start progress timer
	this.mPlayer.mTimer = setInterval(callback.bind(this), 
					  this.mPlayer.mTimerStep);
    };
    // === Update member functions ===================================
    /**
     * Update the progress UI
     */
    CastControl.prototype.updateProgressUI = function() {
	var rel = 0;
	if (this.mMedia.mDuration > 0)
	    rel = Math.ceil(100 * this.mMedia.mTime /
			    this.mMedia.mDuration);
	var txt = this.secondsToString(parseInt(this.mMedia.mTime));
	this.mUI.mSeek.value        = this.mUI.mSeekBg.value = parseInt(rel);
	this.mUI.mCurrent.innerHTML = txt;
    };
    /**
     * Update the volume UI
     */
    CastControl.prototype.updateAudioUI = function() {
	var cnt = parseInt(Math.floor(this.mDevice.mVolume * 100));
	this.mUI.mAudio.value            = this.mUI.mAudioBg.value = cnt;
	this.mUI.mAudioOn.style.display  = (this.mDevice.mMuted?"none":'block');
	this.mUI.mAudioOff.style.display = (this.mDevice.mMuted?"block":'none');
    };
    /**
     * Update display message depending on cast mode by deviceState
     */
    CastControl.prototype.updateDisplayMessage = function() {
	var txt = "?"
	switch (this.mDevice.mState) {
	case DEVICE_STATE.IDLE:    txt = "Idle"; break;
	case DEVICE_STATE.WARNING: txt = "Warnings!"; break;
	case DEVICE_STATE.ERROR:   txt = "Error!"; break;
	case DEVICE_STATE.UNKNOWN: txt = "No ChromeCast found"; break;
	case  DEVICE_STATE.ACTIVE:
	    txt = this.mPlayer.mState + " on " +
		this.mSession.receiver.friendlyName;
	    break;
	}
	this.mUI.mState.innerHTML = txt;
    };

    /**
     * Update media control UI components based on localPlayerState or
     * castPlayerState
     */
    CastControl.prototype.updateMediaControlUI = function() {
	this.mUI.mActive.style.display  = 'none';
	this.mUI.mIdle.style.display    = 'none';
	this.mUI.mWarning.style.display = 'none';
	this.mUI.mError.style.display   = 'none';
	this.mUI.mUnknown.style.display = 'none';

	switch (this.mDevice.mState) {
	case DEVICE_STATE.ACTIVE: 
	    this.mUI.mActive.style.display  = 'block'; break;
	case DEVICE_STATE.IDLE:   
	    this.mUI.mIdle.style.display    = 'block'; break;
	case DEVICE_STATE.WARN:   
	    this.mUI.mWarning.style.display = 'block'; break;
	case DEVICE_STATE.ERROR:  
	    this.mUI.mError.style.display   = 'block'; break;
	case DEVICE_STATE.UNKNOWN:
	    this.mUI.mUnknown.style.display = 'block'; break;
	}
	if (this.mDevice.mState == DEVICE_STATE.ACTIVE)
	    this.mUI.mActive.title = this.mSession.receiver.friendlyName;

	switch (this.mPlayer.mState) {
	case PLAYER_STATE.PLAYING:
	    this.mUI.mPlay.style.display  = 'none';
	    this.mUI.mPause.style.display = 'block';
	    break;
	case PLAYER_STATE.LOADED:
	case PLAYER_STATE.PAUSED:
	case PLAYER_STATE.IDLE:
	case PLAYER_STATE.LOADING:
	case PLAYER_STATE.STOPPED:
	    this.mUI.mPlay.style.display = 'block';
	    this.mUI.mPause.style.display = 'none';
	    break;
	default:
	    break;
	}
    };
    /**
     * Update UI components after selectMedia call
     * @param {Number} mediaIndex An number
     */
    CastControl.prototype.updateMediaInfoUI = function() {
	if (this.mMedia.mInfo) {
	    var dur                      = parseInt(this.mMedia.mDuration);
	    this.mUI.mFile.innerHTML     = this.mMedia.mInfo['title'];
	    this.mUI.mDuration.innerHTML = this.secondsToString(dur);
	}
	else {
	    this.mUI.mFile.innerHTML     = "No media";
	    this.mUI.mDuration.innerHTML = this.secondsToString(0);
	}
    };
    
    /**
     * Initialize UI components and add event listeners
     */
    CastControl.prototype.initializeUI = function() {
	Log.d("Binding to UI elements");
	// set initial values for title, subtitle, and description
	this.mUI.mPlay      = document.getElementById('play');
	this.mUI.mPause     = document.getElementById('pause');
	// Seek interface
	this.mUI.mCurrent   = document.getElementById('current');
	this.mUI.mSeekBar   = document.getElementById('seekBar');
	this.mUI.mSeek      = document.getElementById('seek');
	this.mUI.mSeekBg    = document.getElementById('seekBg');
	this.mUI.mDuration  = document.getElementById('duration');
	// Audio controls
	this.mUI.mAudioOn   = document.getElementById('audioOn');
	this.mUI.mAudioOff  = document.getElementById('audioOff');
	this.mUI.mAudio     = document.getElementById('audio');
	this.mUI.mAudioBg   = document.getElementById('audioBg');
	// Bit rate
	this.mUI.mRate      = document.getElementById('rate');
	// Cast UI
	this.mUI.mIdle      = document.getElementById('idle');
	this.mUI.mError     = document.getElementById('error');
	this.mUI.mWarning   = document.getElementById('warning');
	this.mUI.mActive    = document.getElementById('active');
	this.mUI.mUnknown   = document.getElementById('unknown');
	// Bottom row
	this.mUI.mState     = document.getElementById('state');
	this.mUI.mFile      = document.getElementById('file');

	Log.d("Check for 'live' flag");
	this.mUI.mSeek.addEventListener('click', this.seekMedia.bind(this));
	if (this.mPlayer.mLive) {
	    this.disableSeeks(this.mPlayer.mLive);
	}
	

	// add event handlers to UI components
	Log.d("Adding UI listeners");
	this.mUI.mIdle.addEventListener('click', this.launchApp.bind(this));
	this.mUI.mActive.addEventListener('click', this.stopApp.bind(this));
	this.mUI.mWarning.addEventListener('click', this.stopApp.bind(this));
	this.mUI.mError.addEventListener('click', this.stopApp.bind(this));
	this.mUI.mUnknown.addEventListener('click',
					   this.user.bind(this,
							  "No ChromeCast",""));
	this.mUI.mAudioOn.addEventListener('click', this.muteMedia.bind(this));
	this.mUI.mAudioOff.addEventListener('click', this.muteMedia.bind(this));
	this.mUI.mAudio.addEventListener('click',
					 this.setReceiverVolume.bind(this,
								     false));

	// enable play/pause buttons
	this.mUI.mPlay.addEventListener('click', this.playMedia.bind(this));
	this.mUI.mPause.addEventListener('click', this.pauseMedia.bind(this));

	// Rate
	this.mUI.mRate.addEventListener('onchange', this.updateRate(this));

	// Updates
	this.updateAudioUI();
	Log.d("Done with UI binding");
    };
    // === Utilities =================================================
    /** 
     * Alert the user 
     * 
     * @param lvl Level of message 
     * @param msg Message
     */
    CastControl.prototype.user = function(lvl,msg) {
	alert((lvl?lvl:"")+(lvl&&msg?": ":"")+(msg?msg:""));
    };
    /**
     * Convert a number of seconds into a nice string
     *
     * @param time Number of seconds
     *
     * @return String
     */
    CastControl.prototype.secondsToString = function(time) {
	if (isNaN(time)) return "-:--";
	if (time < 0)  return "-:--";
	if (time == 0) return "0:00";
	var hr = Math.floor(time / 3600);
	var mn = Math.floor((time - hr*3600)/60);
	var sc = Math.floor(time % 60);
	var shr = (hr > 0 ? hr + ":" : "");
	var smn = (hr > 0 && mn < 10 ? "0" : "") + mn + ":";
	var ssc = (sc < 10 ? "0" : "") + sc;

	return (shr + smn + ssc);
    };
    /**
     * Do AJAX call to load media json
     * @param {String} src A URL for media json.
     */
    CastControl.prototype.retrieveMediaJSON = function(src) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener('load', this.onMediaJsonLoad.bind(this));
	xhr.addEventListener('error', this.onMediaJsonError.bind(this));
	xhr.open('GET', src);
	xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
	xhr.responseType = "json";
	xhr.send(null);
    };
    /**
     * Callback function for AJAX call on load success
     * @param {Object} evt An object returned from Ajax call
     */
    CastControl.prototype.onMediaJsonLoad = function(evt) {
	// var responseJson = evt.srcElement.response;
    };

    /**
     * Callback function for AJAX call on load error
     */
    CastControl.prototype.onMediaJsonError = function() {
	Log.w("Failed to load media JSON");
    };


    window.CastControl = CastControl;
})();

/*
 *  EOF
 */
